﻿using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Xml;
using System.Xml.Serialization;
using Blackbird.Common.Exceptions;
using Blackbird.Common.Logging;

namespace Blackbird.Common.Utility {
    public static class SerializationUtility {
        /// <summary>
        /// Reads an element from an XML reader and returns an instance of the appropriate type.
        /// </summary>
        /// <typeparam name="T">The data type of the element stored in XML</typeparam>
        /// <param name="reader">The XML reader to read the element from.</param>
        /// <param name="name">The name of the element to read.</param>
        /// <param name="defaultValue">The default value to return in case the element data cannot be parsed.</param>
        /// <returns></returns>
        public static T ReadElement<T>(XmlReader reader, string name, T defaultValue) {
            try {
                if (reader.Name != name)
                    reader.ReadToFollowing(name);
                return (T)reader.ReadElementContentAs(typeof(T), null);
            } catch (InvalidOperationException invalidOperationException) {
                LogManager.LogMessage(LogEntryType.Error, invalidOperationException.Message);
                return defaultValue;
            }
        }

        /// <summary>
        /// Reads an attribute from an XML reader and returns an instance of the appropriate type.
        /// </summary>
        /// <typeparam name="T">The data type of the attribute stored in XML</typeparam>
        /// <param name="reader">The XML reader to read the attribute from.</param>
        /// <param name="name">The name of the attribute to read.</param>
        /// <param name="defaultValue">The default value to return in case the attribute data cannot be parsed.</param>
        /// <returns></returns>
        public static T ReadAttribute<T>(XmlReader reader, string name, T defaultValue) {
            try {
                string content = reader[name];
                return (T)Convert.ChangeType(content, typeof(T));
            } catch (InvalidOperationException invalidOperationException) {
                LogManager.LogMessage(LogEntryType.Error, invalidOperationException.Message);
                return defaultValue;
            }
        }


        /// <summary>
        /// Creates an instance of type T by deserializing the xml contained in the file at the given path.
        /// </summary>
        /// <typeparam name="T">The type of object to deserialize.</typeparam>
        /// <param name="path">The path to the xml of a serialized object of type T.</param>
        /// <returns>An instance of type T.</returns>
        /// <exception cref="TypeNotSerializableException">The type provided must be made serializable by using the [Serializable] attribute or by implementing the IXmlSerializable interface.</exception>
        [SuppressMessage("Microsoft.Design", "CA1004")]
        public static T CreateFromFile<T>(string path) {
            //Validate the type for serializability
            VerifySerializable(typeof(T));

            //Deserialize the file into a new item.
            T item;
            var serializer = new XmlSerializer(typeof(T));
            using (XmlReader reader = XmlReader.Create(path)) {
                item = (T)serializer.Deserialize(reader);
            }

            return item;
        }

        /// <summary>
        /// Saves an object of type T to file by serializing it to XML.
        /// </summary>
        /// <typeparam name="T">The type of the object to serialize.</typeparam>
        /// <param name="path">The path of the new file. It will be created if it doesn't exists or overwritten if it does.</param>
        /// <param name="itemToSerialize">The object instance of type T to serialize to the file system.</param>
        /// <exception cref="TypeNotSerializableException">The type provided must be made serializable by using the [Serializable] attribute or by implementing the IXmlSerializable interface.</exception>
        public static void SaveToFile<T>(string path, T itemToSerialize) {
            //Validate the type for serializability
            VerifySerializable(typeof(T));

            // Serialize the item
            var serializer = new XmlSerializer(typeof(T));
            using (XmlWriter writer = XmlWriter.Create(path))
            {
                if (writer.Settings != null) writer.Settings.Indent = true;
                serializer.Serialize(writer, itemToSerialize);
            }
        }

        /// <summary>
        /// Serializes an object to a string
        /// </summary>
        /// <typeparam name="T">The type of object to serialize (must be serializable to XML.)</typeparam>
        /// <param name="itemToSerialize">The object to serialize</param>
        /// <returns>A serialized string representing the itemToSerialize as XML.</returns>
        public static string SerializeToString<T>(T itemToSerialize) {
            VerifySerializable(typeof(T));

            var serializer = new XmlSerializer(typeof(T));
            var textWriter = new StringWriter();
            using (var writer = XmlWriter.Create(textWriter))
            {
                if (writer.Settings != null) writer.Settings.Indent = true;
                serializer.Serialize(writer, itemToSerialize);
            }
            return textWriter.ToString();
        }

        /// <summary>
        /// Verifies a types suitablility to be serialized.
        /// </summary>
        /// <param name="t">The type to verify.</param>
        public static void VerifySerializable(Type t) {
            // Examine the provided type            
            object[] attributes = t.GetCustomAttributes(typeof(SerializableAttribute), true);

            // Ensure that the provided type is serializable by attribute or interface
            if (((attributes == null) || (attributes.Length == 0)) &&
                !(t.GetInterfaces().Contains(typeof(IXmlSerializable)))
                && (t.Name != "IXmlSerializable"))
                throw new TypeNotSerializableException();
        }
    }
}